Add-Type -AssemblyName System.Net
$listener = New-Object System.Net.HttpListener
$listener.Prefixes.Add('http://localhost:5500/')
try {
  $listener.Start()
} catch {
  Write-Host "Failed to start HttpListener: $($_.Exception.Message)"; exit 1
}
Write-Host 'Preview URL: http://localhost:5500/'
$root = 'd:\source\ai\todo-web'
function Get-ContentType($path) {
  $ext = [System.IO.Path]::GetExtension($path).ToLower()
  switch ($ext) {
    '.html' { return 'text/html' }
    '.htm' { return 'text/html' }
    '.css' { return 'text/css' }
    '.js' { return 'application/javascript' }
    '.json' { return 'application/json' }
    '.png' { return 'image/png' }
    '.jpg' { return 'image/jpeg' }
    '.jpeg' { return 'image/jpeg' }
    '.svg' { return 'image/svg+xml' }
    default { return 'text/plain' }
  }
}
while ($true) {
  try {
    $ctx = $listener.GetContext()
    $req = $ctx.Request
    $path = $req.Url.LocalPath.TrimStart('/')
    if ([string]::IsNullOrWhiteSpace($path)) { $path = 'index.html' }
    $full = Join-Path $root $path
    if ((Test-Path $full) -and (Get-Item $full).PSIsContainer) { $full = Join-Path $full 'index.html' }
    if (-not (Test-Path $full)) {
      $ctx.Response.StatusCode = 404
      $bytes = [System.Text.Encoding]::UTF8.GetBytes('Not Found')
      $ctx.Response.OutputStream.Write($bytes, 0, $bytes.Length)
    } else {
      $bytes = [System.IO.File]::ReadAllBytes($full)
      $ctx.Response.ContentType = Get-ContentType $full
      $ctx.Response.ContentLength64 = $bytes.Length
      $ctx.Response.OutputStream.Write($bytes, 0, $bytes.Length)
    }
    $ctx.Response.OutputStream.Close()
  } catch {
    Write-Host "Server error: $($_.Exception.Message)"
  }
}